/*
The BSD License (http://www.opensource.org/licenses/bsd-license.php)

Copyright (c) 2007, Niko Kortelainen
All rights reserved.
 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
	* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
	* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
	* Neither the name of the <ORGANIZATION> nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Text;
using System.Net;
using System.Net.Security;

namespace Phalangula
{
	/// <summary>
	/// The DynDNS client updater. Only single host supported and IP resolution is done by the DynDNS resolver.
	/// Also the wildcard, mx and backmx has hard coded value of NOCHG = don't change settings
	/// </summary>
	public class Updater
	{
		private static String VERSION = "0.2.1";
		private static String VENDOR = "Phalangula";
		private static String BINARY_NAME = "Phalangula";
		private static String MODEL = "n/a";
		
		private static char CFG_SPLIT = '=';
		private static String AUTHENTICATION_METHOD = "Basic";
		
		//Configuration file parameters
		private static String CFG_HOST = "host";
		private static String CFG_SERVER = "server";
		private static String CFG_USERNAME = "username";
		private static String CFG_PASSWORD = "password";
		
		//Command line parameters
		private static String PARAM_CFG = "-cfg";
		private static String PARAM_HELP = "-help";
		
		public static void Main(String[] args)
		{
			Updater updater = new Updater();
			
			try
			{
				if(args.Length > 0 && PARAM_HELP.Equals(args[0]))
				{
					updater.printUsage();
				}
				else
				{
					//Get Configuration file either from cmd line of default if not specified
					String configurationFile = updater.getConfigurationFile(args);
					
					//Read the configuration file
					updater.readConfig(configurationFile);
					
					//Do update only if the local address does not match the specified host name
					//This is to avoid the nochg response from DynDNS
					if(!updater.isUpToDate(updater.MyHost))
					{
						log("Current IP is not up-to-date, updating");
						updater.update(updater.MyDynDNSServer, updater.MyHost, updater.MyPassword, updater.MyUserName);					
					}
					else
					{
						log("Current IP is up-to-date");
					}
				}
			}
			catch(Exception e)
			{
				log("Error: " + e.Message);
				updater.printUsage();
			}
		}
		
		/// <summary>
		/// If the configuration file attribute PARAM_CFG is given from cmd line, use the configuration file, 
		/// otherwise use the default phalangula.conf from the same dir the program was started
		/// </summary>
		/// <param name="args">CMD line parameters</param>
		/// <returns>THe configuration file to use</returns>
		private String getConfigurationFile(String[] args)
		{
			for(int i = 0; i < args.Length; i++)
			{
				if(PARAM_CFG.Equals(args[i]))
				{
					if(args.Length > i+1)
					{
						MyConfigurationFile = args[i+1];
					}
					else
					{
						throw new Exception(PARAM_CFG + " needs configuration file");
					}
				}
			}
			
			return MyConfigurationFile;
		}
		
		/// <summary>
		/// Parses the given configuration file
		/// Configuration file example:
		/// ****************
		/// host=myhost.mydomain.com
		/// server=members.dyndns.org
		/// username=myusername
		/// password=mypassword
		/// **************** 
		/// </summary>
		/// <param name="configurationFile">Path to the configuration file</param>
		private void readConfig(String configurationFile)
		{
			StreamReader reader;
			String configurationLine;
    		
    		reader = File.OpenText(configurationFile);
    		
    		configurationLine = reader.ReadLine();
    		while(configurationLine != null)
	    	{
    			if(configurationLine.StartsWith(CFG_HOST))
    			{
    				MyHost = getValue(configurationLine);
    			}
    			else if(configurationLine.StartsWith(CFG_SERVER))
    			{
    				MyDynDNSServer = getValue(configurationLine);
    			}    
    			else if(configurationLine.StartsWith(CFG_USERNAME))
    			{
    				MyUserName = getValue(configurationLine);
    			}  
    			else if(configurationLine.StartsWith(CFG_PASSWORD))
    			{
    				MyPassword = getValue(configurationLine);
    			}      			
    			
    			configurationLine=reader.ReadLine();
    		}
    		reader.Close();
		}
		
		/// <summary>
		/// Gets the configuration value for the given configuration file line
		/// The line must be in format: name = value
		/// </summary>
		/// <param name="confLine">Configuration file line where to get the value</param>
		/// <returns>Value for the given configuration file line</returns>
		private String getValue(String confLine)
		{
			String[] split = confLine.Split(CFG_SPLIT);
			if(split.Length != 2)
			{
				throw new Exception("Invalid configuration line: " + confLine);
			}
			
			return split[1].Trim();
		}
		
		/// <summary>
		/// Does the actual update to the DynDNS service. Uses the DynDNS IP resolving.
		/// </summary>
		/// <param name="server">DynDNS server</param>
		/// <param name="host">Host to configure</param>
		/// <param name="password">Password</param>
		/// <param name="username">User name</param>
		/// <returns></returns>
		private String update(String server, String host, String password, String username)
		{
			//Create allow all policy for SSL connection
			System.Net.ServicePointManager.ServerCertificateValidationCallback += delegate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
			{
				//Don't be a Fox Mulder, trust all
				return true;
			}; 
			
			//Setup basic authentication
			CredentialCache credCache = new CredentialCache();
			credCache.Add(new Uri("https://" + server), AUTHENTICATION_METHOD, 
			              new NetworkCredential(username, password));
			
			//Construct the update url
			StringBuilder url = new StringBuilder();
			url.Append("https://" + server + "/nic/update?");
			url.Append("hostname=" + host);
			url.Append("&wildcard=NOCHG&mx=NOCHG&backmx=NOCHG");
			
			//Execute the update
			log("Invoking: " + url.ToString());
			HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url.ToString());
			request.Credentials = credCache;
			
			//DynDNS requires User-Agent header
			request.UserAgent = VENDOR + " - " + MODEL + " - " + VERSION;
			StreamReader responseStream = new StreamReader(request.GetResponse().GetResponseStream());
			String response = responseStream.ReadToEnd();
			responseStream.Close();
			log("Result: " + response.ToString());		
			
			return response.ToString();
		}
		
		/// <summary>
		/// Check if the current IP is the same that DNS returns for the given host name
		/// </summary>
		/// <param name="hostName">The host to check</param>
		/// <returns>true if the IP is the same as the DNS reported for the host</returns>
		private bool isUpToDate(String hostName)
		{
			bool upToDate = false;
			String localHostName = Dns.GetHostName();
			
			IPHostEntry localIpEntry = Dns.GetHostEntry(localHostName);
			IPHostEntry targetIpEntry = Dns.GetHostEntry(hostName);
			
			IPAddress [] localAddr = localIpEntry.AddressList;
			IPAddress [] targetAddr = targetIpEntry.AddressList;
			
			for (int i = 0; i < localAddr.Length; i++)
			{	
				IPAddress localIp = localAddr[i];
				for(int j = 0; j < targetAddr.Length; j++)
				{
					IPAddress targetIp = targetAddr[j];
					
					if(localIp.ToString().Equals(targetIp.ToString()))
					{
						upToDate = true;
						break;						
					}
				}
				
				if(upToDate)
				{
					break;
				}
			}
			
			return upToDate;
		}
		
		/// <summary>
		/// Print the usage to the console
		/// </summary>
		private void printUsage()
		{
			Console.WriteLine("\n\n" + BINARY_NAME + " version " + VERSION + " usage:");
			Console.WriteLine(BINARY_NAME + ".exe [" + PARAM_CFG + " <path to cfg file>]");
			Console.WriteLine("\t" + PARAM_CFG + "\tPath to the configuration file, defaults to " + MyConfigurationFile);
			Console.WriteLine("\t" + PARAM_HELP + "\tPrint this usage");

			
			Console.WriteLine("\n\nExamples:");
			Console.WriteLine("\t" + BINARY_NAME + ".exe " + PARAM_CFG + " C:\\WhyDidntIUseTheDefault.conf");
		}
		
		private static void log(String txt)
		{
			Console.WriteLine("[" + System.DateTime.Now + "] " + txt);
		}
		
		String MyConfigurationFile = "phalangula.conf";
		String MyDynDNSServer = "members.dyndns.org";
		String MyPassword = null;
		String MyUserName = null;
		String MyHost = null;
	}
}
